ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / feature / firmware / src / commonTest / kotlin / org / meshtastic / feature / firmware / ota / Esp32OtaUpdateHandlerTest.kt
Displaying Raw β’ Download
feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt c2a8e67aee4db8f77b7dc6924dcebfbdfba1bf5e (c2a8e67a) Text, 24.80 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.firmware.ota
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.everySuspend
Tff7b72import T7ee787dev.mokkery.matcher.any
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787kotlinx.coroutines.CoroutineDispatcher
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.runBlocking
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.meshtastic.core.common.util.CommonUri
Tff7b72import T7ee787org.meshtastic.core.database.entity.FirmwareRelease
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.DeviceHardware
Tff7b72import T7ee787org.meshtastic.core.repository.FirmwareUpdateStatusRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeNodeRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioController
Tff7b72import T7ee787org.meshtastic.core.testing.TestDataFactory
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareArtifact
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareFileHandler
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareRetriever
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareUpdateState
Tff7b72import T7ee787org.meshtastic.proto.ClientNotification
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertIs
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue
T8b949e/**
* Tests for [Esp32OtaUpdateHandler]'s OTA preflight gate β the bounded wait for firmware confirmation between
* `triggerRebootOta` and tearing down the mesh transport.
*
* Each test drives `startUpdate` end-to-end just far enough to observe the preflight outcome:
* - Confirmed β mesh disconnect (`setDeviceAddress("n")`) is invoked, then the handler proceeds into the BLE transport
* phase, which fails fast on the mocked BLE deps. We bound the call with `withTimeoutOrNull` so the post-preflight
* BLE-connect retry loop is cancelled before it costs real wall-clock time.
* - Rejected β mesh transport is preserved (no `setDeviceAddress`) and an `Error` state is emitted; the handler returns
* cleanly with no exception to chase.
* - Silent firmware β legacy fallback disconnects the mesh transport and proceeds toward OTA connection attempts.
*
* The firmware response is simulated by mutating [FakeRadioController.clientNotification] from a sibling coroutine
* scheduled to fire after `triggerRebootOta`. Because baseline is captured BEFORE the trigger (see `performUpdate`), a
* value written afterward is recognized as the response, not the baseline.
*/
Tff7b72class T56d364Esp32OtaUpdateHandlerTest Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3release Tff7b72= Te6edf3FirmwareReleaseTb4b4b4(Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffv1.0Ta5d6ff"Tb4b4b4, Te6edf3title Tff7b72= Ta5d6ff"Ta5d6fftestTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3hardware Tff7b72=
Te6edf3DeviceHardwareTb4b4b4(Te6edf3hwModelSlug Tff7b72= Ta5d6ff"Ta5d6ffHELTEC_V3Ta5d6ff"Tb4b4b4, Te6edf3platformioTarget Tff7b72= Ta5d6ff"Ta5d6ffheltec-v3Ta5d6ff"Tb4b4b4, Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffesp32-s3Ta5d6ff"Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3firmwareUri Tff7b72= Te6edf3CommonUriTb4b4b4.Te6edf3parseTb4b4b4(Ta5d6ff"Ta5d6fffile:///tmp/firmware.binTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3dispatchers Tff7b72=
Te6edf3CoroutineDispatchersTb4b4b4(
Te6edf3io Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3main Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3default Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffmakeHandlerTb4b4b4(
Te6edf3radioControllerTb4b4b4: Te6edf3FakeRadioControllerTb4b4b4,
Te6edf3nodeRepositoryTb4b4b4: Te6edf3FakeNodeRepositoryTb4b4b4,
Te6edf3fileHandlerTb4b4b4: Te6edf3FirmwareFileHandlerTb4b4b4,
Te6edf3firmwareUpdateStatusRepositoryTb4b4b4: Te6edf3FirmwareUpdateStatusRepositoryTb4b4b4,
Te6edf3environmentTb4b4b4: Te6edf3TestEsp32OtaUpdateEnvironmentTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3Esp32OtaUpdateHandler Tff7b72= Te6edf3Esp32OtaUpdateHandlerTb4b4b4(
Te6edf3firmwareRetriever Tff7b72= Te6edf3FirmwareRetrieverTb4b4b4(Te6edf3fileHandlerTb4b4b4)Tb4b4b4,
Te6edf3firmwareFileHandler Tff7b72= Te6edf3fileHandlerTb4b4b4,
Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3firmwareUpdateStatusRepository Tff7b72= Te6edf3firmwareUpdateStatusRepositoryTb4b4b4,
Te6edf3environment Tff7b72= Te6edf3environmentTb4b4b4,
Te6edf3bleScanner Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3bleConnectionFactory Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3dispatchersTb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffnewFileHandlerTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FirmwareFileHandler Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4) Tb4b4b4{
Te6edf3everySuspend Tb4b4b4{ Te6edf3readBytesTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3byteArrayOfTb4b4b4(T79c0ff1Tb4b4b4, T79c0ff2Tb4b4b4, T79c0ff3Tb4b4b4, T79c0ff4Tb4b4b4)
Te6edf3everySuspend Tb4b4b4{ Te6edf3importFromUriTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3FirmwareArtifactTb4b4b4(Te6edf3uri Tff7b72= Te6edf3firmwareUriTb4b4b4, Te6edf3fileName Tff7b72= Ta5d6ff"Ta5d6fffirmware.binTa5d6ff"Tb4b4b4, Te6edf3isTemporary Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffnewNodeRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FakeNodeRepository Tff7b72= Te6edf3FakeNodeRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3setMyNodeInfoTb4b4b4(Te6edf3TestDataFactoryTb4b4b4.Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNum Tff7b72= T79c0ff1Tb4b4b4, Te6edf3firmwareVersion Tff7b72= Ta5d6ff"Ta5d6ff1.0Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e/** Schedule [block] on a sibling Default dispatcher; cancelled at the end of the test. */
Tff7b72private Tff7b72fun Td2a8ffrunSideEffectTb4b4b4(Te6edf3blockTb4b4b4: Te6edf3suspend Te6edf3CoroutineScopeTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tb4b4b4: Te6edf3Job Tff7b72=
Te6edf3CoroutineScopeTb4b4b4(Te6edf3DispatchersTb4b4b4.Te6edf3DefaultTb4b4b4)Tb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3blockTb4b4b4(Tb4b4b4) Tb4b4b4}
T8b949e/**
* Shared fixture builder for preflight tests. Schedules a sibling coroutine that emits [confirmationMessage] as a
* firmware ClientNotification after [CONFIRMATION_DELAY_MS]; pass `confirmationMessage = null` to skip the emitter
* entirely and exercise the legacy fallback path.
*/
Tff7b72private Tff7b72fun Td2a8ffrunPreflightTestTb4b4b4(
Te6edf3targetTb4b4b4: Tffa657String Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessageTb4b4b4: Tffa657String? Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3configureTb4b4b4: Te6edf3TestEsp32OtaUpdateEnvironmentTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3runUpdateTb4b4b4: Te6edf3suspend Te6edf3PreflightFixtureTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{
Te6edf3handlerTb4b4b4.Te6edf3startUpdateTb4b4b4(Te6edf3releaseTb4b4b4, Te6edf3hardwareTb4b4b4, Te6edf3targetTb4b4b4, Te6edf3statesTff7b72::Te6edf3addTb4b4b4, Te6edf3firmwareUriTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3assertionsTb4b4b4: Te6edf3PreflightFixtureTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tff7b72= Te6edf3runBlocking Tb4b4b4{
Tff7b72val Te6edf3radioController Tff7b72= Te6edf3FakeRadioControllerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3nodeRepository Tff7b72= Te6edf3newNodeRepositoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3firmwareUpdateStatusRepository Tff7b72= Te6edf3FirmwareUpdateStatusRepositoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3environment Tff7b72= Te6edf3TestEsp32OtaUpdateEnvironmentTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3applyTb4b4b4(Te6edf3configureTb4b4b4)
Tff7b72val Te6edf3handler Tff7b72=
Te6edf3makeHandlerTb4b4b4(
Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3fileHandler Tff7b72= Te6edf3newFileHandlerTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3firmwareUpdateStatusRepository Tff7b72= Te6edf3firmwareUpdateStatusRepositoryTb4b4b4,
Te6edf3environment Tff7b72= Te6edf3environmentTb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3states Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3FirmwareUpdateStateTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3fixture Tff7b72= Te6edf3PreflightFixtureTb4b4b4(Te6edf3handler Tff7b72= Te6edf3handlerTb4b4b4, Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4, Te6edf3states Tff7b72= Te6edf3statesTb4b4b4)
Tff7b72val Te6edf3emitter Tff7b72=
Te6edf3confirmationMessageTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3msg Tff7b72-Tff7b72>
Te6edf3runSideEffect Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3CONFIRMATION_DELAY_MSTb4b4b4)
Te6edf3radioControllerTb4b4b4.Te6edf3setClientNotificationTb4b4b4(Te6edf3ClientNotificationTb4b4b4(Te6edf3message Tff7b72= Te6edf3msgTb4b4b4)Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72try Tb4b4b4{
Te6edf3fixtureTb4b4b4.Te6edf3runUpdateTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3emitterTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3fixtureTb4b4b4.Te6edf3assertionsTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3firmwareUpdateStatusRepositoryTb4b4b4.Te6edf3statusTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3isOtaUpdateActiveTb4b4b4,
Ta5d6ff"Ta5d6ffFirmware update status must reset after the handler finishes or is cancelledTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
T8b949e// ββ Confirmed ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed preflight releases the mesh transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3runUpdate Tff7b72= Tb4b4b4{
T8b949e// Bound the call: we only need to observe the preflight side effect. The post-preflight BLE transport
T8b949e// phase would otherwise burn ~30 s of real wall-clock time on retry/delay loops with mocked BLE deps.
Te6edf3withTimeoutOrNullTb4b4b4(T79c0ff3T79c0ff0T79c0ff0T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3handlerTb4b4b4.Te6edf3startUpdateTb4b4b4(Te6edf3releaseTb4b4b4, Te6edf3hardwareTb4b4b4, Te6edf3BLE_TARGETTb4b4b4, Te6edf3statesTff7b72::Te6edf3addTb4b4b4, Te6edf3firmwareUriTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed BLE preflight retries with fresh transport after connection failure`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transports Tff7b72=
Te6edf3ArrayDequeTff7b72<Te6edf3FakeOtaTransportTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3addTb4b4b4(
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6fffirstTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffOTA service missingTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3addTb4b4b4(Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffsecondTa5d6ff"Tb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3bleTransportFactory Tff7b72= Tb4b4b4{ Te6edf3transportsTb4b4b4.Te6edf3removeFirstTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:firstTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffFailed transport must be closed before retryTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffRetry must create and connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:firstTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:secondTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffSuccessful transport must be closed after transferTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffLater fresh transport should complete updateTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight waits for OTA service readiness before connecting`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= Te6edf3TEST_WIFI_READINESS_DELAY_MS
Te6edf3delayBlock Tff7b72= Tb4b4b4{ Te6edf3delayMs Tff7b72-Tff7b72> Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3delayMsTa5d6ff" Tb4b4b4}
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifiTa5d6ff"Tb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffWiFi post-confirm readiness delay must runTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifiTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi transport must connect after readiness delayTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifiTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffWiFi transport should connect after readiness delayTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight uses UDP discovered address before connecting`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= Te6edf3TEST_WIFI_READINESS_DELAY_MS
Te6edf3delayBlock Tff7b72= Tb4b4b4{ Te6edf3delayMs Tff7b72-Tff7b72> Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3delayMsTa5d6ff" Tb4b4b4}
Te6edf3wifiDiscoveryEnabled Tff7b72= Tff7b72true
Te6edf3discoverWifiOtaDeviceBlock Tff7b72= Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"
Te6edf3DISCOVERED_WIFI_TARGET
Tb4b4b4}
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3targetTa5d6ff"
Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Te6edf3targetTb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi OTA should listen for the loader's DHCP addressTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3DISCOVERED_WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffDiscovered DHCP address must replace the pre-reboot addressTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3DISCOVERED_WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffWiFi transport must connect to the discovered addressTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3WIFI_TARGETTa5d6ff" Tff7b72|Tff7b72| Tffa657it Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3WIFI_TARGETTa5d6ff" Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffConfigured pre-reboot address should not be used after discovery succeedsTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff" Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffDiscovery timeout window counts as the post-confirm readiness waitTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight falls back to configured address when UDP discovery misses`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= Te6edf3TEST_WIFI_READINESS_DELAY_MS
Te6edf3delayBlock Tff7b72= Tb4b4b4{ Te6edf3delayMs Tff7b72-Tff7b72> Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3delayMsTa5d6ff" Tb4b4b4}
Te6edf3wifiDiscoveryEnabled Tff7b72= Tff7b72true
Te6edf3discoverWifiOtaDeviceBlock Tff7b72= Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"
Tff7b72null
Tb4b4b4}
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3targetTa5d6ff"
Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Te6edf3targetTb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi OTA should attempt UDP discovery before fixed-IP fallbackTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffMissing discovery must fall back to configured targetTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffFallback transport must still connectTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffDiscovery miss must still apply the post-confirm readiness marginTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffReadiness delay must run before connectingTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight retries with fresh transport after connection failure`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transports Tff7b72=
Te6edf3ArrayDequeTff7b72<Te6edf3FakeOtaTransportTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3addTb4b4b4(
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-firstTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffConnection refusedTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3addTb4b4b4(Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-secondTa5d6ff"Tb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3transportsTb4b4b4.Te6edf3removeFirstTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:wifi-firstTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffFailed WiFi transport must be closed before retryTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifi-secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi retry must create and connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:wifi-firstTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifi-secondTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:wifi-secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffSuccessful WiFi transport must be closed after transferTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffLater fresh WiFi transport should complete updateTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight closes failed transports and surfaces connection error after retries`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3attempt Tff7b72= T79c0ff0
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{
Te6edf3attempt Tff7b72+Tff7b72= T79c0ff1
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-Tffd700$Te6edf3attemptTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffConnection refusedTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3WIFI_CONNECT_ATTEMPTSTb4b4b4, Te6edf3attemptTb4b4b4, Ta5d6ff"Ta5d6ffWiFi should use the full post-confirm retry budgetTa5d6ff"Tb4b4b4)
Te6edf3repeatTb4b4b4(Te6edf3WIFI_CONNECT_ATTEMPTSTb4b4b4) Tb4b4b4{ Te6edf3index Tff7b72-Tff7b72>
Tff7b72val Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff"
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must close its failed transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstart:Ta5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstream:Ta5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3ErrorTff7b72>Tb4b4b4(Te6edf3statesTb4b4b4.Te6edf3lastOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed BLE preflight closes failed transports and surfaces connection error after retries`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3attempt Tff7b72= T79c0ff0
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3bleTransportFactory Tff7b72= Tb4b4b4{
Te6edf3attempt Tff7b72+Tff7b72= T79c0ff1
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffble-Tffd700$Te6edf3attemptTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffGATT unreachableTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3BLE_CONNECT_ATTEMPTSTb4b4b4, Te6edf3attemptTb4b4b4, Ta5d6ff"Ta5d6ffBLE should use the full post-confirm retry budgetTa5d6ff"Tb4b4b4)
Te6edf3repeatTb4b4b4(Te6edf3BLE_CONNECT_ATTEMPTSTb4b4b4) Tb4b4b4{ Te6edf3index Tff7b72-Tff7b72>
Tff7b72val Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffble-Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff"
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must close its failed transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstart:Ta5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstream:Ta5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3ErrorTff7b72>Tb4b4b4(Te6edf3statesTb4b4b4.Te6edf3lastOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}
T8b949e// ββ Rejected βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tf0883e@Test
Tff7b72fun Td2a8ff`Rejected preflight preserves mesh transport and surfaces Error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runPreflightTestTb4b4b4(
T8b949e// OTA-keyed message that is NOT the canonical "Rebooting to <mode> OTA" success prefix β Rejected.
Te6edf3confirmationMessage Tff7b72= Ta5d6ff"Ta5d6ffCannot start OTA: OTA Loader partition not found.Ta5d6ff"Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffRejected preflight must NOT disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Error Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffRejected preflight must emit Error stateTa5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e// ββ Legacy fallback ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tf0883e@Test
Tff7b72fun Td2a8ff`Silent preflight falls back to legacy OTA reconnect path`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runPreflightTestTb4b4b4(
T8b949e// No emitter (confirmationMessage = null): older firmware may be silent, so the overridden preflight
T8b949e// timeout
T8b949e// should resolve quickly into the same disconnect/reconnect path the app used before preflight
T8b949e// confirmation.
Te6edf3confirmationMessage Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3otaPreflightTimeoutMs Tff7b72= T79c0ff1T79c0ff0L
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Tb4b4b4}Tb4b4b4,
Te6edf3runUpdate Tff7b72= Tb4b4b4{
Te6edf3withTimeoutOrNullTb4b4b4(T79c0ff3T79c0ff0T79c0ff0T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3handlerTb4b4b4.Te6edf3startUpdateTb4b4b4(Te6edf3releaseTb4b4b4, Te6edf3hardwareTb4b4b4, Te6edf3BLE_TARGETTb4b4b4, Te6edf3statesTff7b72::Te6edf3addTb4b4b4, Te6edf3firmwareUriTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(
Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4,
Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4,
Ta5d6ff"Ta5d6ffSilent preflight must still disconnect mesh serviceTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
T8b949e// Valid BLE MAC target so startUpdate routes through the BLE OTA preflight path.
Tff7b72const Tff7b72val Te6edf3BLE_TARGET Tff7b72= Ta5d6ff"Ta5d6ffAA:BB:CC:DD:EE:FFTa5d6ff"
Tff7b72const Tff7b72val Te6edf3WIFI_TARGET Tff7b72= Ta5d6ff"Ta5d6ff192.168.1.33Ta5d6ff"
Tff7b72const Tff7b72val Te6edf3DISCOVERED_WIFI_TARGET Tff7b72= Ta5d6ff"Ta5d6ff192.168.1.44Ta5d6ff"
Tff7b72const Tff7b72val Te6edf3BLE_CONFIRMATION Tff7b72= Ta5d6ff"Ta5d6ffRebooting to BLE OTATa5d6ff"
Tff7b72const Tff7b72val Te6edf3WIFI_CONFIRMATION Tff7b72= Ta5d6ff"Ta5d6ffRebooting to WiFi OTATa5d6ff"
Tff7b72const Tff7b72val Te6edf3CONFIRMATION_DELAY_MS Tff7b72= T79c0ff5T79c0ff0L
Tff7b72const Tff7b72val Te6edf3BLE_CONNECT_ATTEMPTS Tff7b72= T79c0ff5
Tff7b72const Tff7b72val Te6edf3WIFI_CONNECT_ATTEMPTS Tff7b72= T79c0ff1T79c0ff0
Tff7b72const Tff7b72val Te6edf3TEST_WIFI_READINESS_DELAY_MS Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4L
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72data Tff7b72class T56d364PreflightFixtureTb4b4b4(
Tff7b72val Te6edf3handlerTb4b4b4: Te6edf3Esp32OtaUpdateHandlerTb4b4b4,
Tff7b72val Te6edf3radioControllerTb4b4b4: Te6edf3FakeRadioControllerTb4b4b4,
Tff7b72val Te6edf3statesTb4b4b4: Te6edf3MutableListTff7b72<Te6edf3FirmwareUpdateStateTff7b72>Tb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72class T56d364TestEsp32OtaUpdateEnvironment Tb4b4b4: Te6edf3Esp32OtaUpdateEnvironment Tb4b4b4{
Tff7b72override Tff7b72var Te6edf3otaPreflightTimeoutMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff5Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3otaTransportRetryDelayMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff2Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3wifiOtaReadinessDelayMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff8Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3gattReleaseDelayMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff1Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3wifiDiscoveryEnabledTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false
T8b949e// Fully qualified: an unqualified `delay(it)` would resolve to this class's own override (member functions win
T8b949e// over imported top-level functions), producing infinite recursion through delayBlock β StackOverflowError.
Tff7b72var Te6edf3delayBlockTb4b4b4: Te6edf3suspend Tb4b4b4(Tffa657LongTb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{ Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3delayTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tff7b72var Te6edf3bleTransportFactoryTb4b4b4: Tb4b4b4(Tb4b4b4(Tffa657StringTb4b4b4) Tff7b72-Tff7b72> Te6edf3UnifiedOtaProtocolTb4b4b4)Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3wifiTransportFactoryTb4b4b4: Tb4b4b4(Tb4b4b4(Tffa657StringTb4b4b4) Tff7b72-Tff7b72> Te6edf3UnifiedOtaProtocolTb4b4b4)Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3discoverWifiOtaDeviceBlockTb4b4b4: Te6edf3suspend Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657String? Tff7b72= Tb4b4b4{ Tff7b72null Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffdelayTb4b4b4(Te6edf3millisecondsTb4b4b4: Tffa657LongTb4b4b4) Tb4b4b4{
Te6edf3delayBlockTb4b4b4(Te6edf3millisecondsTb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffcreateBleTransportTb4b4b4(
Te6edf3bleScannerTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3bleTb4b4b4.Te6edf3BleScannerTb4b4b4,
Te6edf3bleConnectionFactoryTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3bleTb4b4b4.Te6edf3BleConnectionFactoryTb4b4b4,
Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3dispatcherTb4b4b4: Te6edf3CoroutineDispatcherTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3UnifiedOtaProtocol Tff7b72=
Te6edf3bleTransportFactoryTff7b72?.Te6edf3invokeTb4b4b4(Te6edf3addressTb4b4b4) Tff7b72?: Te6edf3BleOtaTransportTb4b4b4(Te6edf3bleScannerTb4b4b4, Te6edf3bleConnectionFactoryTb4b4b4, Te6edf3addressTb4b4b4, Te6edf3dispatcherTb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffcreateWifiTransportTb4b4b4(Te6edf3deviceIpTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3UnifiedOtaProtocol Tff7b72=
Te6edf3wifiTransportFactoryTff7b72?.Te6edf3invokeTb4b4b4(Te6edf3deviceIpTb4b4b4) Tff7b72?: Te6edf3WifiOtaTransportTb4b4b4(Te6edf3deviceIpTb4b4b4)
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffdiscoverWifiOtaDeviceTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Te6edf3discoverWifiOtaDeviceBlockTb4b4b4.Te6edf3invokeTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72class T56d364FakeOtaTransportTb4b4b4(
Tff7b72private Tff7b72val Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72private Tff7b72val Te6edf3eventsTb4b4b4: Te6edf3MutableListTff7b72<Tffa657StringTff7b72>Tb4b4b4,
Tff7b72private Tff7b72val Te6edf3connectResultTb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3UnifiedOtaProtocol Tb4b4b4{
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffconnectTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"
Tff7b72return Te6edf3connectResult
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffstartOtaTb4b4b4(
Te6edf3sizeBytesTb4b4b4: Tffa657LongTb4b4b4,
Te6edf3sha256HashTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3onHandshakeStatusTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3OtaHandshakeStatusTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstart:Tffd700$Te6edf3nameTa5d6ff"
Tff7b72return Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffstreamFirmwareTb4b4b4(
Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4,
Te6edf3chunkSizeTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3onProgressTb4b4b4: Te6edf3suspend Tb4b4b4(Tffa657FloatTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstream:Tffd700$Te6edf3nameTa5d6ff"
Te6edf3onProgressTb4b4b4(T79c0ff1fTb4b4b4)
Tff7b72return Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.13s